package org.erikaredmark.monkeyshines.tiles; import java.awt.Graphics2D; import org.erikaredmark.monkeyshines.GameConstants; import org.erikaredmark.monkeyshines.GameSoundEffect; import org.erikaredmark.monkeyshines.Hazard; import org.erikaredmark.monkeyshines.resource.SoundManager; import org.erikaredmark.monkeyshines.resource.WorldResource; /** * * Indicates a hazard on the map, along with drawing and other state information. Whilst {@code Hazard} represents the * information describing a type of hazard, this object represents one such type somewhere on the world * <p/> * As with other tiles, the actual location in the level is maintained in a tilemap, not in this object. * * @author Erika Redmark * */ public class HazardTile implements TileType { // The actual hazard that this tile represents (is this a lava hazard/bomb? Some other type for this world?) // This is also the only thing serialized; everything else is state information for during gameplay private final Hazard hazard; // State information // Determines if the hazard should be in exploding animation or not. Ignored for hazards that don't explode and // ignored for a hazard that already exploded. private boolean exploding = false; // Cycles between 0-1 for basic drawing, and then 0-9 for explosion rendering. // When this value is -1, the hazard is not drawn. Additionally, a value of -1 // indicates the hazard is 'dead' and can't hurt bonzo anymore. private int animationPoint = 0; // When this reaches TICKS_BETWEEN_ANIMATIONS, it goes back to zero and the animationPoint is updated. // This prevents quick, rapid animation. private int timeToNextFrame = 0; private static final int TICKS_BETWEEN_ANIMATIONS = 5; private static final int MAX_EXPLODING_FRAMES = 8; private HazardTile(final Hazard hazard) { this.hazard = hazard; } /** * * Creates a hazard on a tile for the given hazard. * * @param h * the hazard type this map hazard represents * * @return * an instance of this object * */ public static final HazardTile forHazard(Hazard h) { return new HazardTile(h); } /** * * Returns the hazard type that this tile is representing * * @return * the hazard * */ public Hazard getHazard() { return hazard; } public boolean isExploding() { return exploding; } /** * * Returns the frame of animation that should be used in the current sprite sheet to draw this hazard. * <p/> * Note that if this hazard is exploding, the exploding sprite sheet should be used instead of the standard * one. The animation step is always 0 based; it is up to caller to determine which actual sprite sheet * needs to be used for drawing. * <p/> * It is an error to call this method if the hazard is destroyed (it shouldn't be drawn at all). If assertions * are disabled calling the method in this state returns -1 which has undefined behaviour. * * @return * animation step for the current sprite sheet, dependent on whether this hazard is exploding or not. * */ public int getAnimationStep() { assert !(isDead() ); return animationPoint; } @Override public int getId() { return hazard.getId(); } /** * * updates animation tick for drawing * */ @Override public void update() { if (isDead() ) return; if (readyToAnimate() ) { if (!exploding) { animationPoint = animationPoint == 0 ? 1 : 0; } else { ++animationPoint; if (animationPoint > MAX_EXPLODING_FRAMES) die(); } } } @Override public boolean isThru() { return false; } @Override public boolean isSolid() { return false; } @Override public boolean isLandable() { return isSolid() || isThru(); } @Override public void paint(Graphics2D g2d, int drawToX, int drawToY, WorldResource rsrc) { // Nothing to paint if dead. if (isDead() ) return; // If exploding, paint the explosions instead: if (!(isExploding() ) ) { hazard.paint(g2d, drawToX, drawToY, rsrc, getAnimationStep() ); } else { int animation = getAnimationStep(); g2d.drawImage(rsrc.getExplosionSheet(), drawToX, drawToY, drawToX + GameConstants.TILE_SIZE_X, drawToY + GameConstants.TILE_SIZE_Y, animation * GameConstants.TILE_SIZE_X, 0, (animation + 1) * GameConstants.TILE_SIZE_X, GameConstants.TILE_SIZE_Y, null); } } /** * * Checks if the hazard is ready to switch to the next frame of animation. Calling this * method changes state; it checks if ready, and if not increments the ticker. As defined * in the class static final variables, a certain number of 'ticks' have to pass before * being allowed to change the animation state. * <p/> * Hazards animate slower normally, and quicker when exploding. * * @return * {@code true} if ready, {@code false} if otherwise. Merely calling this method * increments the ticker, so this should only be called in the main update method * on each tick. * */ private boolean readyToAnimate() { if (isExploding() ) return true; if (timeToNextFrame >= TICKS_BETWEEN_ANIMATIONS) { timeToNextFrame = 0; return true; } else { ++timeToNextFrame; return false; } } /** * * Kills the hazard, preventing it from harming bonzo anymore. * */ private void die() { animationPoint = -1; } /** * * Sets the animation step for this hazard, either 0 for the first row or 1 for the second row. Other values * will cause undefined behaviour. * <p/> * Intended to stagger animation of hazards in adjacent tiles. * * @param step * 0 for the first row of animation, 1 for the second. * */ public void setAnimationStep(final int step) { } /** * * Resets the state of the hazard. Normally called whenever a level screen is reloaded. * */ @Override public void reset(boolean oddElseEven) { exploding = false; animationPoint = oddElseEven ? 1 : 0; timeToNextFrame = 0; } @Override public TileType copy() { return new HazardTile(hazard); } /** * * For hazard tiles that use a hazard that explodes, this represents whether the hazard is gone (it should no longer * affect bonzo), or is still alive. Hazards that don't explode never die. * * @return * {@code true} if the hazard is gone and should not be used in bonzo death considerations, {@code false} if otherwise * */ public boolean isDead() { return animationPoint == -1; } /** * * Called when bonzo hits the hazard. This does not kill or affect bonzo; it affects the state of the hazard (exploding * hazards must now begin explodin). Does nothing if the hazard is dead. * <p/> * Requires a sound manager in case the hazard needs to play a sound upon collision. * */ public void hazardHit(SoundManager soundManager) { if (isDead() ) return; if (hazard.explodes() ) { exploding = true; animationPoint = 0; soundManager.playOnce(GameSoundEffect.EXPLOSION); } } /** * * Equality of a hazard tile depends on only the underlying hazard type, not state information. This is important: an exploded * bomb (hazard id 0) is equal to an unexploded bomb (hazard id 0). * */ @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof HazardTile) ) return false; HazardTile other = (HazardTile) o; return hazard.equals(other.hazard); } @Override public int hashCode() { int result = 17; result += result * 31 + hazard.hashCode(); return result; } @Override public String toString() { return hazard.toString() + ": exploding state is " + exploding + " and hazard life status is " + isDead(); } }